package tech

import (
	"slices"
	"ssp/set"
)

func (s *Solve) AlmostLockCandidates(g *Grid) (used bool, results []*Result) {
	for boxNum := 0; boxNum < 9; boxNum++ {
		box := g.boxes[boxNum]
		rowStart := boxNum / 3 * 3
		colStart := boxNum % 3 * 3
		for rowNum := rowStart; rowNum < rowStart+3; rowNum++ {
			row := g.rows[rowNum]
			cells := row.cells[colStart : colStart+3]
			if ok, result := alcHouses(cells, box, row); ok {
				used = true
				results = append(results, &result)
			}
			if ok, result := alcHouses(cells, row, box); ok {
				used = true
				results = append(results, &result)
			}
		}
		for colNum := colStart; colNum < colStart+3; colNum++ {
			col := g.cols[colNum]
			cells := col.cells[rowStart : rowStart+3]

			if ok, result := alcHouses(cells, box, col); ok {
				used = true
				results = append(results, &result)
			}
			if ok, result := alcHouses(cells, col, box); ok {
				used = true
				results = append(results, &result)
			}
		}
	}
	return
}

func alcHouses(shares []*Cell,
	nakedHouse, hiddenHouse House) (ok bool, result Result) {
	nakedCells := make([]*Cell, 0)
	for _, cell := range nakedHouse.cells {
		if cell.solved() || slices.Contains(shares, cell) {
			continue
		}
		nakedCells = append(nakedCells, cell)
	}

	hiddenCells := make([]*Cell, 0)
	for _, cell := range hiddenHouse.cells {
		if cell.solved() || slices.Contains(shares, cell) {
			continue
		}
		hiddenCells = append(hiddenCells, cell)
	}

	for i := 1; i < min(4, len(nakedCells)); i++ {
		for _, nakedAls := range combine(nakedCells, i) {
			nakedCands := unionCellCands(nakedAls)
			if nakedCands.Count() != i+1 {
				continue
			}
			hiddenAls := make([]*Cell, 0)
			otherCands := unionCellCands(shares).Diff(nakedCands)
			for _, cell := range hiddenCells {
				if cell.cands.Intersect(nakedCands).Count() != 0 {
					hiddenAls = append(hiddenAls, cell)
				} else {
					otherCands = otherCands.Union(cell.cands)
				}
			}
			if len(hiddenAls) == len(hiddenCells) {
				continue
			}

			hiddenCands := unionCellCands(hiddenAls).Diff(otherCands)

			if !hiddenCands.IsSuperset(nakedCands) || len(hiddenAls)+1 != hiddenCands.Count() {
				continue
			}
			//fmt.Println("naken, hidden", nakedCells, hiddenCells)
			rmNakedCells := make([]*Cell, 0)
			for _, cell := range nakedCells {
				if slices.Contains(nakedAls, cell) {
					continue
				}
				if cell.canRemoveCands(nakedCands) {
					rmNakedCells = append(rmNakedCells, cell)
					ok = true
				}
			}
			rmHiddenCells := make([]*Cell, 0)
			rmHiddenCands := set.NewFull().Diff(hiddenCands)
			for _, cell := range hiddenAls {
				if cell.canRemoveCands(rmHiddenCands) {
					rmHiddenCells = append(rmHiddenCells, cell)
					ok = true
				}
			}
			if !ok {
				continue
			}
			if OUTPUTRESULT {
				result.addRemoved(nakedCands, rmNakedCells)
				result.addRemoved(rmHiddenCands, rmHiddenCells)
				g1 := result.getHelpCandsGroup()
				g1.add(nakedCands, nakedAls)
				g2 := result.getHelpCandsGroup()
				g2.add(hiddenCands, hiddenAls)
				result.addHelpHouses([]*House{&nakedHouse})
				result.addHelpHouses([]*House{&hiddenHouse})
				result.setStrategyName("Almost Locked Candidates")
			}
			if CHANGE {
				removeCands(nakedCands, rmNakedCells)
				removeCands(rmHiddenCands, rmHiddenCells)
			}
			if !EXHAUSTIVE {
				return
			}
		}
	}

	return
}

func (s *Solve) AlmostLockedSetsXZ(g *Grid) (used bool, results []*Result) {
	for _, alss := range combine(g.getAllALS(), 2) {
		alsa := alss[0]
		alsb := alss[1]
		if slices.Equal(alsa, alsb) {
			continue
		}
		xs := findALS_X(alsa, alsb)
		switch len(xs) {
		case 0:
			continue
		case 1:
			candsa := unionCellCands(alsa)
			candsb := unionCellCands(alsb)
			zs := candsa.Intersect(candsb).Diff(set.NewSet(xs...)).GetList()
			xcands := set.NewSet(xs[0])
			for _, z := range zs {
				if ok, rmCells := g.canRemoveALSXZ(z, alsa, alsb); ok {
					zcands := set.NewSet(z)
					used = true
					result := new(Result)
					result.setStrategyName("Almost Locked Sets XZ")
					result.addRemoved(zcands, rmCells)
					result.addHelpCells(alsa)
					result.addHelpCells(alsb)
					g1 := result.getHelpCandsGroup()
					g1.add(xcands, append(alsa, alsb...))
					g2 := result.getHelpCandsGroup()
					g2.add(zcands, append(alsa, alsb...))
					results = append(results, result)
					//fmt.Println("1 switch xs", xs)
				}
			}
		default:
			//for _, xz := range [][]int{{xs[0], xs[1]}, {xs[1], xs[0]}} {
			x := xs[0]
			z := xs[1]
			xcands := set.NewSet(x)
			zcands := set.NewSet(z)
			result := new(Result)
			result.setStrategyName("Almost Locked Sets XZ")
			result.addHelpCells(alsa)
			result.addHelpCells(alsb)
			g1 := result.getHelpCandsGroup()
			g1.add(xcands, append(alsa, alsb...))
			g2 := result.getHelpCandsGroup()
			g2.add(zcands, append(alsa, alsb...))
			if ok, rmCells := g.canRemoveALSXZ(z, alsa, alsb); ok {
				used = true
				result.addRemoved(zcands, rmCells)
			}
			if ok, rmCells := g.canRemoveALSXZ(x, alsa, alsb); ok {
				used = true
				result.addRemoved(xcands, rmCells)
			}

			for _, als := range [][]*Cell{alsa, alsb} {
				for _, y := range unionCellCands(als).Diff(set.NewSet(x, z)).GetList() {
					ycands := set.NewSet(y)
					if ok, rmCells := g.canRemoveALSXZPlus(ycands, als); ok {
						used = true
						result.addRemoved(ycands, rmCells)
					}
				}
			}
			if used {
				results = append(results, result)
			}
		}
	}
	return
}

func findALS_X(alsa, alsb []*Cell) (xs []int) {
	candsa := unionCellCands(alsa)
	candsb := unionCellCands(alsb)
	xcands := candsa.Intersect(candsb)
	shares := shareCells(alsa, alsb)
	if len(shares) > 0 {
		xcands = xcands.Diff(unionCellCands(shares))
	}
	if xcands.Count() == 0 {
		return
	}

	for _, x := range xcands.GetList() {
		if !seenBetween(x, alsa, alsb) {
			continue
		}
		xs = append(xs, x)
	}

	return
}

func seenBetween(cand int, alsa, alsb []*Cell) bool {
	for _, cella := range alsa {
		if !cella.cands.Has(cand) {
			continue
		}
		for _, cellb := range alsb {
			if !cellb.cands.Has(cand) {
				continue
			}
			if !isSameHouse([]*Cell{cella, cellb}) {
				return false
			}
		}
	}
	return true
}

func (g *Grid) canRemoveALSXZ(z int, alsa, alsb []*Cell) (
	used bool, removeCells []*Cell) {
	zcells := []*Cell{}
	for _, cell := range alsa {
		if cell.cands.Has(z) {
			zcells = append(zcells, cell)
		}
	}
	for _, cell := range alsb {
		if cell.cands.Has(z) {
			zcells = append(zcells, cell)
		}
	}

	return g.canRemoveSeenCands(set.NewSet(z), zcells)
}

func (g *Grid) canRemoveALSXZPlus(z set.Set, alsa []*Cell) (
	used bool, removeCells []*Cell) {
	zcells := []*Cell{}
	for _, cell := range alsa {
		if !cell.cands.Intersect(z).IsNull() {
			zcells = append(zcells, cell)
		}
	}

	return g.canRemoveSeenCands(z, zcells)
}

func (s *Solve) ALSXYWing(g *Grid) (used bool, results []*Result) {
	return g.alsXYWing()
}

func (g *Grid) alsXYWing3(alsa, alsb, alsc []*Cell) (used bool, results []*Result) {
	xs := findALS_X(alsa, alsb)
	ys := findALS_X(alsa, alsc)

	for _, x := range xs {
		for _, y := range ys {
			if x == y {
				continue
			}
			zCands1 := unionCellCands(alsb).Diff(set.NewSet(x))
			zCands2 := unionCellCands(alsc).Diff(set.NewSet(y))
			zCands := zCands1.Intersect(zCands2)
			for _, z := range zCands.GetList() {
				if ok, rmCells := g.canRemoveALSXYWing(z, alsb, alsc); ok {
					used = true
					result := new(Result)
					result.setStrategyName("ALS-XY-Wing")
					zCand := set.NewSet(z)
					result.addRemoved(zCand, rmCells)
					result.addHelpCells(alsa)
					result.addHelpCells(alsb)
					result.addHelpCells(alsc)
					g1 := result.getHelpCandsGroup()
					g1.add(set.NewSet(x), append(alsa, alsb...))
					g2 := result.getHelpCandsGroup()
					g2.add(set.NewSet(y), append(alsa, alsc...))
					g3 := result.getHelpCandsGroup()
					g3.add(zCand, append(alsb, alsc...))
					results = append(results, result)
					//fmt.Println(x, y, z)
					return
				}
			}
		}
	}
	return
}
func (g *Grid) alsXYWing() (used bool, results []*Result) {
	allAls := g.getAllALS()
	//shuffle(g, allAls)
	for _, alss := range combine(allAls, 3) {
		used, results = g.alsXYWing3(alss[0], alss[1], alss[2])
		if used {
			return
		}
		/*
			used, results = g.alsXYWing3(alss[1], alss[0], alss[2])
			if used {
				return
			}
			used, results = g.alsXYWing3(alss[2], alss[1], alss[0])
			if used {
				return
			}*/
	}
	return
}

func (g *Grid) canRemoveALSXYWing(z int, alsb, alsc []*Cell) (used bool, removeCells []*Cell) {
	zcells := []*Cell{}
	for _, cell := range alsb {
		if cell.cands.Has(z) {
			zcells = append(zcells, cell)
		}
	}
	for _, cell := range alsc {
		if cell.cands.Has(z) {
			zcells = append(zcells, cell)
		}
	}

	return g.canRemoveSeenCands(set.NewSet(z), zcells)

}
